////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.logging
{

import mx.logging.errors.InvalidCategoryError;
import mx.managers.ISystemManager;
import mx.managers.SystemManager;
import mx.resources.ResourceBundle;

/**
 *  Provides psuedo-hierarchical logging capabilities with multiple format and
 *  output options.
 *  The log system consists of two major components, the logger and a target.
 *  You can use the logger to send information to a target.
 *  The target is responsible for formatting and general output of the log data.
 *  <p>
 *  Loggers are singleton instances created for a particular category of
 *  information.
 *  Typically, the category is the package name of the component
 *  that desires to log information.
 *  The category provides users the ability to specify what log information they
 *  are interested in.
 *  Multiple categories can be selected and combined with regular expressions.
 *  This allows for both broad and narrow logging information to be acquired.
 *  For example, you might be interested in all logging information under
 *  the "mx.messaging" and "mx.rpc" packages and want the output from these
 *  packages to be formatted as xml.
 *  To get the all of the logging information under the "mx.messaging" category
 *  including sub-packages and components a wildcard expression is required, such as
 *  "mx.messaging.~~".
 *  See the code example below for more details.
 *  </p>
 *  <p>Targets provide the output mechanism of the data being logged.
 *  This mechanism typically includes formatting, transmission, or storage, but,
 *  can be anything possible under the VM.
 *  There are several different targets provided: <code>XMLLogTarget</code>,
 *  <code>MiniDebugTarget</code>, <code>DebugChannelTarget</code>,
 *  <code>TraceTarget</code>, etc.
 *  Each of these writers take the current log information and "sends" it
 *  somewhere for display and/or storage.
 *  Targets also provide the specification of what log data to output.
 *  </p>
 *
 *  @example
 *  <pre>
 *  ...
 *  import mx.logging.ILoggingTarget;
 *  import mx.logging.Log;
 *  import mx.logging.LogEvent;
 *  import mx.logging.targets.TraceTarget;
 *  // Set up writer:
 *  var traceTarget:ILoggingTarget = new TraceTarget();
 *  traceTarget.filters = ["mx.messaging.~~", "mx.rpc.~~"];
 *  traceTarget.level = LogEventLevel.DEBUG;
 *  // Start writing log data:
 *  Log.addTarget(traceTarget);
 *
 *  // Get a logger for the mx.messaging.Channel "category"
 *  // and send some data to it:
 *  var logger:ILogger = Log.getLogger("mx.messaging.Channel"); 
 *  logger.log("Here is some channel info {0} and {1}", LogEventLevel.DEBUG, 15.4, true);
 * // The output logged would look like this:
 * // Here is some channel info 15.4 and true
 *  // because the xmlTarget is listening for anything under mx.messaging
 *  // with the level of DEBUG it will get data logged in this example.
 *  ...
 *  </pre>
 */
public class Log
{
	include "../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Class initialization
	//
	//--------------------------------------------------------------------------

	loadResources();

	//--------------------------------------------------------------------------
	//
	//  Class resources
	//
	//--------------------------------------------------------------------------

	[ResourceBundle("logging")]

    /**
	 *  @private
     */	
	private static var packageResources:ResourceBundle;

    /**
	 *  @private
     */	
	private static var resourceInvalidTarget:String;

    /**
	 *  @private
     */	
	private static var resourceInvalidLen:String;

    /**
	 *  @private
     */	
	private static var resourceInvalidChars:String;

	//--------------------------------------------------------------------------
	//
	//  Class variables
	//
	//--------------------------------------------------------------------------

    /**
     *  @private
     *  Sentinal value for the target log level to indicate no logging.
     */
    private static var NONE:int = int.MAX_VALUE;

    /**
     *  @private
     *  The most verbose supported log level among registered targets.
     */
    private static var _targetLevel:int = NONE;
		// Initialize target level to a value out of range.

    /**
     *  @private
     *  An associative Array of existing loggers keyed by category
     */
    private static var _loggers:Array;

    /**
     *  @private
     *  Array of targets that should be searched any time
	 *  a new logger is created.
     */
    private static var _targets:Array = [];

	//--------------------------------------------------------------------------
	//
	//  Class methods
	//
	//--------------------------------------------------------------------------

    /**
	 *  @private    
     *  Loads resources for this class.
     */
	private static function loadResources():void
	{
		resourceInvalidTarget = packageResources.getString("invalidTarget");

		resourceInvalidLen = packageResources.getString("invalidLen");

		resourceInvalidChars = packageResources.getString("invalidChars");
	}
		
	/**
	 *  Indicates whether a fatal level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a fatal level log event will be logged; otherwise false.
	 */
	public static function isFatal():Boolean
	{
	    return (_targetLevel <= LogEventLevel.FATAL) ? true : false;
	}
	
	/**
	 *  Indicates whether an error level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if an error level log event will be logged; otherwise false.
	 */
	public static function isError():Boolean
	{
	    return (_targetLevel <= LogEventLevel.ERROR) ? true : false;
	}
	
	/**
	 *  Indicates whether a warn level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a warn level log event will be logged; otherwise false.
	 */
	public static function isWarn():Boolean
	{
	    return (_targetLevel <= LogEventLevel.WARN) ? true : false;
	}

	/**
	 *  Indicates whether an info level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if an info level log event will be logged; otherwise false.
	 */	
	public static function isInfo():Boolean
	{
	    return (_targetLevel <= LogEventLevel.INFO) ? true : false;
	}
	
	/**
	 *  Indicates whether a debug level log event will be processed by a
	 *  log target.
	 *
	 *  @return true if a debug level log event will be logged; otherwise false.
	 */
	public static function isDebug():Boolean
	{
	    return (_targetLevel <= LogEventLevel.DEBUG) ? true : false;
	}

    /**
     *  Allows the specified target to begin receiving notification of log
     *  events.
     *
     *  @param The specific target that should capture log events.
     */
    public static function addTarget(target:ILoggingTarget):void
    {
        if(target)
        {
            var filters:Array = target.filters;
            var logger:ILogger;
            // need to find what filters this target matches and set the specified
            // target as a listener for that logger.
            for(var i:String in _loggers)
            {
                if(categoryMatchInFilterList(i, filters))
                    target.addLogger(ILogger(_loggers[i]));
            }
            // if we found a match all is good, otherwise we need to
            // put the target in a waiting queue in the event that a logger
            // is created that this target cares about.
            _targets.push(target);
            
            if (_targetLevel == NONE)
                _targetLevel = target.level
            else if (target.level < _targetLevel)
                _targetLevel = target.level;
        }
        else
            throw new ArgumentError(resourceInvalidTarget);
    }

    /**
     *  Stops the specified target from receiving notification of log
     *  events.
     *
     *  @param The specific target that should capture log events.
     */
    public static function removeTarget(target:ILoggingTarget):void
    {
        if(target)
        {
            var filters:Array = target.filters;
            var logger:ILogger;
            // Disconnect this target from any matching loggers.
            for(var i:String in _loggers)
            {
                if(categoryMatchInFilterList(i, filters))
                {
                    target.removeLogger(ILogger(_loggers[i]));
                }                
            }
            // Remove the target.
            for(var j:int = 0; j<_targets.length; j++)
            {
                if(target == _targets[j])
                {
                    _targets.splice(j, 1);
                    j--;
                }
            }
            resetTargetLevel();
        }
        else
            throw new ArgumentError(resourceInvalidTarget);
    }

    /**
     *  Returns the logger associated with the specified category.
     *  If the category given doesn't exist a new instance of a logger will be
     *  returned and associated with that category.
     *  Categories must be at least one character in length and may not contain
     *  any blanks or any of the following characters:
     *  []~$^&amp;\/(){}&lt;&gt;+=`!#%?,:;'"&#64;
     *  This method will throw an <code>InvalidCategoryError</code> if the
     *  category specified is malformed.
     *
     *  @param category The category of the logger that should be returned.
	 *
     *  @return An instance of a logger object for the specified name.
	 *  If the name doesn't exist, a new instance with the specified
	 *  name is returned.
     */
    public static function getLogger(category:String):ILogger
    {
        checkCategory(category);
        if(!_loggers)
            _loggers = [];

        // get the logger for the specified category or create one if it
        // doesn't exist
        var result:ILogger = _loggers[category];
        if(result == null)
        {
            result = new LogLogger(category);
            _loggers[category] = result;
        }

        // check to see if there are any targets waiting for this logger.
        var target:ILoggingTarget;
        for(var i:int=0; i<_targets.length; i++)
        {
            target = ILoggingTarget(_targets[i]);
            if(categoryMatchInFilterList(category, target.filters))
                target.addLogger(result);
        }  // for

        return result;
    }

    /**
     *  This method removes all of the current loggers from the cache.
     *  Subsquent calls to the <code>getLogger()</code> method return new instances
     *  of loggers rather than any previous instances with the same category.
     *  This method is intended for use in debugging only.
     */
    public static function flush():void
    {
        _loggers = [];
        _targets = [];
        _targetLevel = NONE;
    }

    /**
     *  This method checks the specified string value for illegal characters.
     *
     *  @param value The String to check for illegal characters.
     *            The following characters are not valid:
     *                []~$^&amp;\/(){}&lt;&gt;+=`!#%?,:;'"&#64;
     *  @return   <code>true</code> if there are any illegal characters found,
     *            <code>false</code> otherwise
     */
    public static function hasIllegalCharacters(value:String):Boolean
    {
        return value.search(/[\[\]\~\$\^\&\\(\)\{\}\+\?\/=`!@#%,:;'"<>\s]/) != -1;
    }

    // private members
    /**
     *  This method checks that the specified category matches any of the filter
     *  expressions provided in the <code>filters</code> Array.
     *
     *  @param category The category to match against
     *  @param filters A list of Strings to check category against.
     *  @return <code>true</code> if the specified category matches any of the
     *            filter expressions found in the filters list, <code>false</code>
     *            otherwise.
     *  @private
     */
    private static function categoryMatchInFilterList(category:String, filters:Array):Boolean
    {
        var result:Boolean = false;
        var filter:String;
        var index:int = -1;
        for(var i:uint = 0; i<filters.length; i++)
        {
            filter = filters[i];
            // first check to see if we need to do a partial match
            // do we have an asterisk?
            index = filter.indexOf("*");

            if(index == 0)
                return true;

            index = index < 0 ? index = category.length : index -1;

            if(category.substring(0, index) == filter.substring(0, index))
                return true;
        }
        return false;
    }

    /**
     *  This method will ensure that a valid category string has been specified.
     *  If the category is not valid an <code>InvalidCategoryError</code> will
     *  be thrown.
     *  Categories can not contain any blanks or any of the following characters:
     *    []`*~,!#$%^&amp;()]{}+=\|'";?&gt;&lt;./&#64; or be less than 1 character in length.
     *  @private
     */
    private static function checkCategory(category:String):void
    {
        if(category == null || category.length == 0)
            throw new InvalidCategoryError(resourceInvalidLen);

        if(hasIllegalCharacters(category) || (category.indexOf("*") != -1))
            throw new InvalidCategoryError(resourceInvalidChars);
    }
    
    /**
     *  @private
     *  This method resets the Log's target level to the most verbose log level
     *  for the currently registered targets.
     */
    private static function resetTargetLevel():void
    {
        var minLevel:int = NONE;
        for (var i:int = 0; i < _targets.length; i++)
        {
            if (minLevel == NONE || _targets[i].level < minLevel)
                minLevel = _targets[i].level;
        }
        _targetLevel = minLevel;
    }
}

}
